Functional Domain Modeling in Kotlin

您所在的位置:网站首页 kotlin enum Functional Domain Modeling in Kotlin

Functional Domain Modeling in Kotlin

2023-03-12 21:35| 来源: 网络整理| 查看: 265

This article was originally published at 47deg.com on February 11, 2021.

At Xebia, we care a lot about domain modeling to describe our domain as precisely as possible.

The goal of functional domain modeling is to describe your business domain as accurately as possible to achieve more type-safety, maximize the use of the compiler with our domain and, thus, prevent bugs and reduce unit testing. Additionally, it makes it easier to communicate about the domain, since the domain is the touchpoint with the real world.

Kotlin is a good fit for functional domain modeling. It offers us data class, sealed class, enum class, and inline class. And we have Arrow, which offers us some interesting generic data types such as Either, Validated, Ior, etc.

In some codebases, you can find the following primitive type based implementation of an Event:

data class Event( val id: Long val title: String, val organizer: String, val description: String, val date: LocalDate )

The types used here have little or no meaning since title, organizer, and description all share the same type.

This makes our code prone to subtle bugs where we might be relying on title instead of description, and the compiler would not be able to help us out.

Let’s take a look at an example where things go wrong without the compiler being able to help us.

Event( 0L, "Simon Vergauwen", "In this blogpost we dive into functional DDD...", "Functional Domain Modeling", LocalDate.now() )

Here, we have mixed up organizer, description, but the compiler is happy and constructs the Event object. There are more cases where you can fall into this trap; for example, when destructuring.

So how do we prevent this from happening, or how can we improve our domain model to be better typed? Let’s use a still experimental, but very exciting, upcoming feature of Kotlin: inline class. Doing this causes no additional overhead, since inline class is erased at runtime. (You can replace this with data class if you don’t want to rely on @Experimentalcode>@Experimental println(event.url.value) is AtAddress -> println("${event.address.city}: ${event.address.street}") }

This type of data composition is also known as a sum type, which models an OR relationship, but sealed class offers us more powerful capabilities than enum class. A sealed class allows our sum or cases to exist out of object, data class, or even another sealed class. An enum class cannot extend another class, so it cannot be a case of a sealed class. Here, our sealed class exists out of 2 cases, an Online OR AtAddress Event, where Online and AtAddress are a product types of several other types. A rule of thumb in Kotlin is to use an enum class when the cases don’t contain any data or, in other words, if all cases can be modeled as object.

As we’ve already seen in the examples above, modeling your domain precisely has many benefits. It can eliminate certain bugs, such as instantiating data incorrectly. It makes our code/model easier to reason about by eliminating invalid values, and it can improve code relying on our models.

Let’s take a look at how we can use Arrow’s data types to further clear up domain problems in our code. In our program, we have some EventService that can fetch an upcoming Event based on a EventId.

interface EventService { suspend fun fetchUpcomingEvent(id: EventId): Event }

What is completely missing from our EventService is the different kind of error scenarios we could encounter. It’s only modeled through Throwable in suspend. So if we’d want to explicitly model the error domain, we could use any of the techniques we’ve already seen above.

Here, we model 2 different cases:

An event is not found.An event is no longer upcoming, but has already happened.sealed class Error { data class EventNotFound(val id: EventId): Error() data class EventPassed(val event: Event): Error() }

We can compose these two separate domains, Error and Event, using Either from Arrow Core. This allows us to model an OR relationship, meaning that fetchUpcomingEvent either returns an Error or an Event, but never both. So Either is a generic sum type, which allows us to generically compose two separate domains with each other in an OR relationship.

So, if we update our EventService:

interface EventService { suspend fun fetchUpcomingEvent(id: EventId): Either }

Since Either is defined as sealed class in Arrow Core, we can use the same technique as we used above with when to extract the Error or Event in a safe way. Arrow Core is a module by itself, so you can fetch it like so:

depdendencies { def arrowVersion = "0.11.0" implementation "io.arrow-kt:arrow-core:$arrowVersion" }

In this post, we’ve seen how we can improve our domain by:

Eliminating primitive types in our domain definition, and using inline class to prevent runtime overhead.Using enum class and sealed class to model disjunctions in our domain, such as certain data being available depending on the type of Event.Utilizing Arrow’s Either to compose two different domains with an OR relationship.

In future blog posts, we’ll discuss how we can improve our domain models even more by using other types of Arrow, and using other techniques such as type refinement to constrain our models even further.



【本文地址】


今日新闻


推荐新闻


    CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3